﻿#region License, Terms and Author(s)
//
// ELMAH - Error Logging Modules and Handlers for ASP.NET
// Copyright (c) 2004-9 Atif Aziz. All rights reserved.
//
//  Author(s):
//
//      Laimonas Simutis, mailto:laimis@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

[assembly: Elmah.Scc("$Id: PgsqlErrorLog.cs 925 2011-12-23 22:46:09Z azizatif $")]

namespace Elmah
{
    #region Imports

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Collections;
    using Npgsql;
    using NpgsqlTypes;

    #endregion

    /// <summary>
    /// An <see cref="ErrorLog"/> implementation that uses PostgreSQL
    /// as its backing store.
    /// </summary>
    /// 
    public class PgsqlErrorLog : ErrorLog
    {
        private readonly string _connectionString;

        /// <summary>
        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class
        /// using a dictionary of configured settings.
        /// </summary>

        public PgsqlErrorLog(IDictionary config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            string connectionString = ConnectionStringHelper.GetConnectionString(config);

            //
            // If there is no connection string to use then throw an 
            // exception to abort construction.
            //

            if (connectionString.Length == 0)
                throw new ApplicationException("Connection string is missing for the Postgres SQL error log.");

            _connectionString = connectionString;

            //
            // Set the application name as this implementation provides
            // per-application isolation over a single store.
            //

            string appName = config.Find("applicationName", string.Empty);

            if (appName.Length > Commands.MaxAppNameLength)
            {
                throw new ApplicationException(string.Format(
                    "Application name is too long. Maximum length allowed is {0} characters.",
                    Commands.MaxAppNameLength.ToString("N0")));
            }

            ApplicationName = appName;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class
        /// to use a specific connection string for connecting to the database.
        /// </summary>

        public PgsqlErrorLog(string connectionString)
        {
            if (connectionString == null)
                throw new ArgumentNullException("connectionString");

            if (connectionString.Length == 0)
                throw new ArgumentException(null, "connectionString");
            
            _connectionString = connectionString;
        }

        /// <summary>
        /// Gets the name of this error log implementation.
        /// </summary>
        
        public override string Name
        {
            get { return "PostgreSQL Error Log"; }
        }

        /// <summary>
        /// Gets the connection string used by the log to connect to the database.
        /// </summary>
        
        public virtual string ConnectionString
        {
            get { return _connectionString; }
        }

        public override string Log(Error error)
        {
            if (error == null)
                throw new ArgumentNullException("error");

            string errorXml = ErrorXml.EncodeString(error);
            Guid id = Guid.NewGuid();

            using (NpgsqlConnection connection = new NpgsqlConnection(ConnectionString))
            using (NpgsqlCommand command = Commands.LogError(id, this.ApplicationName, error.HostName, error.Type, error.Source, error.Message, error.User, error.StatusCode, error.Time, errorXml))
            {
                command.Connection = connection;
                connection.Open();
                command.ExecuteNonQuery();
                return id.ToString();
            }
        }

        public override ErrorLogEntry GetError(string id)
        {
            if (id == null) throw new ArgumentNullException("id");
            if (id.Length == 0) throw new ArgumentException(null, "id");

            Guid errorGuid;

            try
            {
                errorGuid = new Guid(id);
            }
            catch (FormatException e)
            {
                throw new ArgumentException(e.Message, "id", e);
            }

            string errorXml;

            using (NpgsqlConnection connection = new NpgsqlConnection(ConnectionString))
            using (NpgsqlCommand command = Commands.GetErrorXml(ApplicationName, errorGuid))
            {
                command.Connection = connection;
                connection.Open();
                errorXml = (string)command.ExecuteScalar();
            }

            if (errorXml == null)
                return null;

            Error error = ErrorXml.DecodeString(errorXml);
            return new ErrorLogEntry(this, id, error);
        }

        public override bool SupportsSearching
        {
            get { return true; }
        }

        public override int GetErrors(SearchCriteria criteria, ICollection<ErrorLogEntry> errorEntryList)
        {
            ValidateCriteria(criteria);

            using (NpgsqlConnection connection = new NpgsqlConnection(ConnectionString))
            {
                connection.Open();

                using (NpgsqlCommand cmd = GetErrorsCommand(ApplicationName, criteria)) 
                {
                    cmd.Connection = connection;

                    using (NpgsqlDataReader reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            string id = reader.GetString(0);
                            string xml = reader.GetString(1);
                            Error error = ErrorXml.DecodeString(xml);
                            errorEntryList.Add(new ErrorLogEntry(this, id, error));
                        }
                    }
                }

                using (NpgsqlCommand cmd = GetErrorsTotalCommand(ApplicationName, criteria))
                {
                    cmd.Connection = connection;
                    return Convert.ToInt32(cmd.ExecuteScalar());
                }
            }
        }

        private NpgsqlCommand GetErrorsCommand(string applicationName, SearchCriteria criteria)
        {
            if (criteria.ContainsExtendedSearchCriteria)
            {
                return Commands.SearchErrorsXml(applicationName, criteria);
            }
            else
            {
                return Commands.GetErrorsXml(applicationName, criteria.PageIndex, criteria.PageSize);
            }
        }

        private NpgsqlCommand GetErrorsTotalCommand(string applicationName, SearchCriteria criteria)
        {
            if (criteria.ContainsExtendedSearchCriteria)
            {
                return Commands.SearchErrorsXmlTotal(applicationName, criteria);
            }
            else
            {
                return Commands.GetErrorsXmlTotal(applicationName);
            }
        }

        private static class Commands
        {
            public const int MaxAppNameLength = 60;

            /// <summary>
            /// Factory-type class to create parameters of the correct names and sizes.
            /// </summary>
            private static class MakeParam
            {
                public static NpgsqlParameter ErrorId(Guid value)
                {
                    return new NpgsqlParameter("ErrorId", value);
                }

                public static NpgsqlParameter Offset(int value)
                {
                    return new NpgsqlParameter("@offset", NpgsqlDbType.Integer) { Value = value };
                }

                public static NpgsqlParameter Limit(int value)
                {
                    return new NpgsqlParameter("@limit", NpgsqlDbType.Integer) { Value = value };
                }

                public static NpgsqlParameter Application(string value)
                {
                    return new NpgsqlParameter("@Application", NpgsqlDbType.Text, MaxAppNameLength) { Value = value };
                }

                public static NpgsqlParameter Host(object value)
                {
                    return new NpgsqlParameter("Host", value);
                }

                public static NpgsqlParameter Type(object value)
                {
                    return new NpgsqlParameter("Type", value);
                }

                public static NpgsqlParameter Source(object value)
                {
                    return new NpgsqlParameter("Source", value);
                }

                public static NpgsqlParameter Message(object value)
                {
                    return new NpgsqlParameter("Message", value);
                }

                public static NpgsqlParameter User(object value)
                {
                    return new NpgsqlParameter("User", value);
                }

                public static NpgsqlParameter StatusCode(int value)
                {
                    return new NpgsqlParameter("StatusCode", value);
                }

                public static NpgsqlParameter StatusCodeString(object value)
                {
                    return new NpgsqlParameter("StatusCodeString", NpgsqlDbType.Text, 100) { Value = value };
                }

                public static NpgsqlParameter TimeUtc(DateTime value)
                {
                    return new NpgsqlParameter("TimeUtc", value);
                }

                public static NpgsqlParameter TimeUtcFrom(object value)
                {
                    return new NpgsqlParameter("TimeUtcFrom", value);
                }

                public static NpgsqlParameter TimeUtcTo(object value)
                {
                    return new NpgsqlParameter("TimeUtcTo", value);
                }

                public static NpgsqlParameter AllXml(object value)
                {
                    return new NpgsqlParameter("AllXml", value);
                }
            }


            public static NpgsqlCommand LogError
                (
                Guid id, string appName, string hostName, string typeName, string source,
                string message, string user, int statusCode, DateTime time, string xml
                )
            {
                NpgsqlCommand cmd = new NpgsqlCommand();
                cmd.CommandText = 
                    @"
                    INSERT INTO Elmah_Error (ErrorId, Application, Host, Type, Source, Message, ""User"", StatusCode, TimeUtc, AllXml)
                    VALUES (@ErrorId, @Application, @Host, @Type, @Source, @Message, @User, @StatusCode, @TimeUtc, @AllXml)
                    ";

                cmd.Parameters.Add(MakeParam.ErrorId(id));
                cmd.Parameters.Add(MakeParam.Application(appName));
                cmd.Parameters.Add(MakeParam.Host(hostName));
                cmd.Parameters.Add(MakeParam.Type(typeName));
                cmd.Parameters.Add(MakeParam.Source(source));
                cmd.Parameters.Add(MakeParam.Message(message));
                cmd.Parameters.Add(MakeParam.User(user));
                cmd.Parameters.Add(MakeParam.StatusCode(statusCode));
                cmd.Parameters.Add(MakeParam.TimeUtc(time.ToUniversalTime()));
                cmd.Parameters.Add(MakeParam.AllXml(xml));

                return cmd;
            }

            public static NpgsqlCommand GetErrorXml(string appName, Guid id)
            {
                NpgsqlCommand cmd = new NpgsqlCommand();

                cmd.CommandText = 
                    @"
                    SELECT AllXml FROM Elmah_Error 
                    WHERE 
                        Application = @Application 
                        AND ErrorId = @ErrorId
                    ";

                cmd.Parameters.Add(MakeParam.Application(appName));
                cmd.Parameters.Add(MakeParam.ErrorId(id));

                return cmd;
            }

            public static NpgsqlCommand GetErrorsXml(string appName, int pageIndex, int pageSize)
            {
                NpgsqlCommand cmd = new NpgsqlCommand();

                cmd.CommandText =
                    @"
                    SELECT ErrorId, AllXml FROM Elmah_Error
                    WHERE
                        Application = @Application
                    ORDER BY Sequence DESC
                    OFFSET @offset
                    LIMIT @limit
                    ";

                cmd.Parameters.Add(MakeParam.Application(appName));
                cmd.Parameters.Add(MakeParam.Offset(pageIndex * pageSize));
                cmd.Parameters.Add(MakeParam.Limit(pageSize));

                return cmd;
            }

            internal static NpgsqlCommand SearchErrorsXml(string appName, SearchCriteria criteria)
            {
                NpgsqlCommand cmd = new NpgsqlCommand();

                cmd.CommandText =
                    @"
                    SELECT ErrorId, AllXml
                    FROM Elmah_Error
                    WHERE
                        (Application ILIKE @Application OR @Application IS NULL)
                        AND (Host ILIKE @Host OR @Host IS NULL)
                        AND (Type ILIKE @Type OR @Type IS NULL)
                        AND (Source ILIKE @Source OR @Source IS NULL)
                        AND (Message ILIKE @Message OR @Message IS NULL)
                        AND (""User"" ILIKE @User OR @User IS NULL)
                        AND (CAST(StatusCode AS VARCHAR) ILIKE @StatusCodeString OR @StatusCodeString IS NULL)
                        AND (TimeUtc >= @TimeUtcFrom OR @TimeUtcFrom IS NULL)
                        AND (TimeUtc < @TimeUtcTo OR @TimeUtcTo IS NULL)
                        AND (AllXml ILIKE @AllXml OR @AllXml IS NULL)
                    ORDER BY Sequence DESC
                    OFFSET @offset
                    LIMIT @limit
                    ";

                cmd.Parameters.Add(MakeParam.Application(appName));
                cmd.Parameters.Add(MakeParam.Host(SqlUtils.GetSearchString(criteria.Host)));
                cmd.Parameters.Add(MakeParam.Type(SqlUtils.GetSearchString(criteria.TypeName)));
                cmd.Parameters.Add(MakeParam.Source(SqlUtils.GetSearchString(criteria.Source)));
                cmd.Parameters.Add(MakeParam.Message(SqlUtils.GetSearchString(criteria.Message)));
                cmd.Parameters.Add(MakeParam.User(SqlUtils.GetSearchString(criteria.User)));
                cmd.Parameters.Add(MakeParam.StatusCodeString(SqlUtils.GetSearchString(criteria.StatusCode)));
                cmd.Parameters.Add(MakeParam.TimeUtcFrom(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcFrom)));
                cmd.Parameters.Add(MakeParam.TimeUtcTo(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcTo)));
                cmd.Parameters.Add(MakeParam.AllXml(SqlUtils.GetSearchString(criteria.AllXml)));
                cmd.Parameters.Add(MakeParam.Offset(criteria.PageIndex * criteria.PageSize));
                cmd.Parameters.Add(MakeParam.Limit(criteria.PageSize));

                return cmd;
            }

            public static NpgsqlCommand GetErrorsXmlTotal(string appName)
            {
                NpgsqlCommand cmd = new NpgsqlCommand();
                cmd.CommandText = "SELECT COUNT(*) FROM Elmah_Error WHERE Application = @Application";
				cmd.Parameters.Add(MakeParam.Application(appName));
                return cmd;
            }

            public static NpgsqlCommand SearchErrorsXmlTotal(string appName, SearchCriteria criteria)
            {
                NpgsqlCommand cmd = new NpgsqlCommand();

                cmd.CommandText =
                    @"
                    SELECT COUNT(*)
                    FROM Elmah_Error
                    WHERE
                        (Application ILIKE @Application OR @Application IS NULL)
                        AND (Host ILIKE @Host OR @Host IS NULL)
                        AND (Type ILIKE @Type OR @Type IS NULL)
                        AND (Source ILIKE @Source OR @Source IS NULL)
                        AND (Message ILIKE @Message OR @Message IS NULL)
                        AND (""User"" ILIKE @User OR @User IS NULL)
                        AND (CAST(StatusCode AS VARCHAR) ILIKE @StatusCodeString OR @StatusCodeString IS NULL)
                        AND (TimeUtc >= @TimeUtcFrom OR @TimeUtcFrom IS NULL)
                        AND (TimeUtc < @TimeUtcTo OR @TimeUtcTo IS NULL)
                        AND (AllXml ILIKE @AllXml OR @AllXml IS NULL)
                    ";


                cmd.Parameters.Add(MakeParam.Application(appName));
                cmd.Parameters.Add(MakeParam.Host(SqlUtils.GetSearchString(criteria.Host)));
                cmd.Parameters.Add(MakeParam.Type(SqlUtils.GetSearchString(criteria.TypeName)));
                cmd.Parameters.Add(MakeParam.Source(SqlUtils.GetSearchString(criteria.Source)));
                cmd.Parameters.Add(MakeParam.Message(SqlUtils.GetSearchString(criteria.Message)));
                cmd.Parameters.Add(MakeParam.User(SqlUtils.GetSearchString(criteria.User)));
                cmd.Parameters.Add(MakeParam.StatusCodeString(SqlUtils.GetSearchString(criteria.StatusCode)));
                cmd.Parameters.Add(MakeParam.TimeUtcFrom(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcFrom)));
                cmd.Parameters.Add(MakeParam.TimeUtcTo(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcTo)));
                cmd.Parameters.Add(MakeParam.AllXml(SqlUtils.GetSearchString(criteria.AllXml)));

                return cmd;
            }
        }
    }
}
